Spring Boot之统一异常处理

如何在Spring Boot应用中构建简单的Controller层统一异常处理框架。

Exception

首先是自定义Exception。作为web app,我们假设Exception针对的是Controller层面,因此需要code字段表示HTTP响应的状态码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javax.servlet.http.HttpServletResponse;
public class MyException extends Exception {

private Integer code;
public MyException(Integer code, String message) {
super(message);
this.code = code;
}
public MyException(String message) {
super(message);
this.code = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
}
public MyException(String message, Throwable t) {
super(message, t);
this.code = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
}
public MyException(Integer code, String message, Throwable t) {
super(message, t);
this.code = code;
}
... // getter & setter
}

同时定义一个ErrorInfo类,用于将错误信息以Json格式返回给调用方。

1
2
3
4
5
6
7
8
9
public class ErrorInfo <T>{
@JsonProperty
public Integer code;
@JsonProperty
private T message;
@JsonProperty
private String url;
... // getters & setters
}

ExceptionHandler

通过@RestControllerAdvice注解,我们定义一个ExceptionHandler,根据RestController抛出的特定Exception,构造HTTP响应的状态码及响应体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@RestControllerAdvice
public class GlobalExceptionHandler {
// 只处理MyException
// 设置状态码,并将errorInfo作为响应体
@ExceptionHandler(value = MyException.class)
@ResponseBody
public ErrorInfo<String> errorHandler(HttpServletRequest req, HttpServletResponse resp, MyException e) throws Exception {
ErrorInfo<String> errorInfo = new ErrorInfo<>();
errorInfo.setMessage(e.getMessage());
errorInfo.setCode(e.getCode());
errorInfo.setUrl(req.getRequestURL().toString());
resp.setStatus(e.getCode());
return errorInfo;
}
}

Controller

到了Controller层面,开发者按需抛出自定义Exception即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@RestController
@RequestMapping("/api/login")
public class LoginController {
... // 其他依赖
private static final Logger logger = LoggerFactory.getLogger(LoginController.class);
@RequestMapping(method = RequestMethod.GET)
public LoginResponse UMLogin(@RequestParam String user,
@RequestParam String pwd,
@RequestParam(required = false) Boolean pwdEncoded,

HttpServletRequest request) throws MyException {

try {
if(null != pwdEncoded && pwdEncoded) {
pwd = new String(Base64.decodeBase64(pwd));
}
... // 其他准备工作
// 验证用户名及密码
LoginResult loginResult = PasswordUtil.checkUserAndPassword(user, pwd);
if(null != loginResult && loginResult.getRetCode() == 0) {
logger.info("用户{} 鉴权通过.", user);
... // 其他善后工作
return new LoginResponse(user, MyConstants.LOGIN_RESULT_SUCCESS);
} else {
logger.info("用户鉴权失败: {}", user, PasswordUtil.getErrorMessage(loginResult, user));
// 抛出自定义Exception
throw new MyException(HttpServletResponse.SC_FORBIDDEN, "用户" + user + "鉴权失败");
}
} catch (Exception e) {
String msg = "鉴权过程异常 ";
logger.error(msg, e);
// 转换后抛出自定义Exception
if(e instanceof MyException) {
throw (MyException) e;
} else {
throw new MyException(msg);
}
}
}
}

局限

最重要的局限在于对于Spring框架层及Interceptor层的异常无法捕获,需要单独处理。